﻿using System;
using System.Activities;
using System.Activities.DurableInstancing;
using System.Activities.Tracking;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.DurableInstancing;
using System.ServiceProcess;
using System.Threading;
using System.Xml.Linq;
using Microsoft.Activities;
using Reactor.Entities;
using Reactor.Environment;
using Reactor.Extensions;
using Reactor.Messages.Commands.Core;
using Reactor.Messages.Events.Adoptions;
using Reactor.Messages.Events.Failures;
using Reactor.Messaging;
using Reactor.Providers;
using Reactor.ServiceGrid.Exceptions;
using Reactor.ServiceGrid.Workflows;
using Reactor.ServiceGrid.Workflows.Contexts;
using Reactor.ServiceGrid.Workflows.Persistence;
using Reactor.Workflow.Extensions;
using Reactor.Workflow.Persistence;
using Reactor.Workflow.Tracking;
using Samurai.Wakizashi.Workflow;

namespace Reactor.ServiceGrid.ServiceEntities
{
    public class ReactorCore : ReactorServiceBase, IReactorCore
    {
        #region Fields

        private readonly ICoreDataProvider _coreDataProvider;
        private readonly ConcurrentBag<ServiceController> _serviceControllers = new ConcurrentBag<ServiceController>();
        private PointToPointChannel _primaryInputChannel;
        private ISequentialWorkflow<MonitorContext> _serviceMonitorWorkflow;

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="ReactorCore"/> class.
        /// </summary>
        /// <param name="coreDataProvider">The core data provider.</param>
        public ReactorCore(ICoreDataProvider coreDataProvider)
        {
            if (coreDataProvider == null) throw new ArgumentNullException("coreDataProvider");
            _coreDataProvider = coreDataProvider;

            CreateIdentifierForThisInstance();

            if (Context.GridContext.CurrentReactorService == null) Context.GridContext.CurrentReactorService = this;
            _coreDataProvider.UpdateCoreLastSeenTimestamp(Identifier);

            Started += ReactorCoreStarted;
        }

        /// <summary>
        /// Gets or sets the services.
        /// </summary>
        /// <value>The services.</value>
        public IEnumerable<ReactorServiceEntity> GetManagedServices()
        {
            return _coreDataProvider.GetAllServiceInstancesAssignedToCore(Identifier).ToArray();
        }

        /// <summary>
        /// Gets all <see cref="ServiceController"/> instances assigned to this Core.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<ServiceController> GetServiceControllers()
        {
            return _serviceControllers.ToArray();
        }

        /// <summary>
        /// Instructs the Core to adopt the specified service. This method creates
        /// and invokes the workflow designated for service orphaning.
        /// </summary>
        /// <param name="serviceIdentifier">The service identifier.</param>
        public void AdoptService(ServiceIdentifier serviceIdentifier)
        {
            RunNewAdoptionWorkflow(serviceIdentifier);
        }

        /// <summary>
        /// Instructs the Core to orphan the specified service. This method creates
        /// and starts the workflow designated for service orphaning.
        /// </summary>
        /// <param name="serviceIdentifier">The service identifier.</param>
        /// <param name="serviceInstanceIdentifier">The service instance identifier.</param>
        public void OrphanService(ServiceIdentifier serviceIdentifier, Guid serviceInstanceIdentifier)
        {
            RunNewOrphanWorkflow(serviceIdentifier, serviceInstanceIdentifier);
        }

        /// <summary>
        /// Instructs the Core to restart the Windows Service hosting the specified 
        /// Reactor Service.
        /// </summary>
        /// <param name="serviceIdentifier">The service identifier.</param>
        public void RestartService(ServiceIdentifier serviceIdentifier)
        {
            var svc = GetServiceControllerByName(serviceIdentifier.ToServiceInstanceString());
            if(svc == null)
            {
                // Send failure advisory message stating that we couldnt find the service
                SendRestartServiceFailureAdvisoryMessage(serviceIdentifier, "no such Windows Service could be found locally");
                return;
            }
            
            // Attempt to restart service. If it fails send advisory message stating why
            string failureMessage;
            var restartTimeout = TimeSpan.FromMinutes(1);

            if(!svc.TryRestart(restartTimeout, out failureMessage))
                SendRestartServiceFailureAdvisoryMessage(serviceIdentifier, failureMessage);
        }

        /// <summary>
        /// Instructs the Core instance to shudown the specified managed service.
        /// </summary>
        /// <param name="serviceIdentifier">The service identifier.</param>
        public void ShutdownService(ServiceIdentifier serviceIdentifier)
        {
            var shutdownWorkflow = new ShutdownServiceWorkflow(serviceIdentifier);
            shutdownWorkflow.OnFailed(c =>
                                          {
                                              var advisoryMessage = new ShutdownServiceFailed
                                                                        {
                                                                            CoreName = Identifier.Name,
                                                                            ServiceName = serviceIdentifier.Name,
                                                                            ServiceVersion = serviceIdentifier.Version.ToString(),
                                                                            Subject = "Shutdown service command failed",
                                                                            Description = string.Empty
                                                                        };
                                              Context.ServiceBus.Send(advisoryMessage);
                                          });
            shutdownWorkflow.Start();
        }

        /// <summary>
        /// Instructs the Core instance to transfer a service from it's own control to
        /// another Core's control.
        /// </summary>
        /// <param name="transferCommand">The transfer command.</param>
        public void TransferService(TransferService transferCommand)
        {
            using (var resetEvent = new AutoResetEvent(false))
            {
                dynamic inputs = new WorkflowArguments();
                inputs.TransferServiceCommand = transferCommand;

                var workflowApp = GetTransferServiceWorkflow(resetEvent, inputs);
                workflowApp.Run();

                resetEvent.WaitOne(TimeSpan.FromSeconds(3));
            }
        }

        /// <summary>
        /// Resumes a previously started service transfer workflow from the point where the donor Core 
        /// is notified that the recipient Core has adopted the target service.
        /// </summary>
        /// <param name="adoptedEvent">The adopted event.</param>
        /// <param name="workflowId">The id of the workflow orchestrating the transfer process.</param>
        public void ResumeServiceTransfer(ServiceAdopted adoptedEvent, Guid workflowId)
        {
            if(workflowId == Guid.Empty)
            {
                Log.ErrorFormat("Unable to resume service transfer with supplied ServiceAdopted event because the workflowId was empty. Service that was adopted: {0}-{1}", adoptedEvent.ServiceName, adoptedEvent.Version);
                return;
            }

            var workflowApp = GetTransferServiceWorkflow();
            var loaded = TryLoadWorkflowInstanceWithRetry(workflowApp, workflowId);

            if (!loaded)
            {
                // Try again with refreshed WorkflowApplication (required)
                workflowApp = GetTransferServiceWorkflow();
                loaded = TryLoadWorkflowInstanceWithRetry(workflowApp, workflowId);

                if (!loaded)
                {
                    // Still not loading after at least 4 seconds. The original workflow is 
                    //  not available and this is an error.
                    throw new TransferServiceException(string.Format("Failure during service transfer while attempting to resume TransferServiceWorkflow. Target Service: {0} | Workflow Id: {1} Failure occured on Core: {2}.", adoptedEvent.ServiceName, workflowId, Identifier.Name));
                }
            }

            var resumptionResult = workflowApp.ResumeBookmark(Bookmarks.AdoptedEventReceived, adoptedEvent);
            switch (resumptionResult)
            {
                case BookmarkResumptionResult.Success:
                    break;
                case BookmarkResumptionResult.NotFound:
                    Log.ErrorFormat("Could not resume bookmark 'AdoptedEventReceived' because it could not be found.");
                    break;
                case BookmarkResumptionResult.NotReady:
                    Log.ErrorFormat("Could not resume bookmark 'AdoptedEventReceived' because the runtime has not yet created the bookmark.");
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <summary>
        /// Resumes a previously started service transfer workflow from the point where the recipient 
        /// Core receives the command to adopt the target service.
        /// </summary>
        /// <param name="adoptServiceCommand">The adopt service command.</param>
        /// <param name="workflowInstanceId">The id of the workflow orchestrating the transfer process.</param>
        public void ResumeServiceTransfer(AdoptService adoptServiceCommand, Guid workflowInstanceId)
        {
            using(var resetEvent = new AutoResetEvent(false))
            {
                var workflowApp = GetTransferServiceWorkflow(resetEvent);
                var loaded = TryLoadWorkflowInstanceWithRetry(workflowApp, workflowInstanceId);

                if (!loaded)
                {
                    // Try again with refreshed WorkflowApplication (required)
                    workflowApp = GetTransferServiceWorkflow(resetEvent);
                    loaded = TryLoadWorkflowInstanceWithRetry(workflowApp, workflowInstanceId);

                    if (!loaded)
                    {
                        // Still not loading after at least 4 seconds. The original workflow is 
                        //  not available and this is an error.
                        throw new TransferServiceException(string.Format("Failure during service adoption while attempting to resume TransferServiceWorkflow. Target Service: {0}-{1} | Workflow Id: {2} Failure occured on Core: {3}.", adoptServiceCommand.ServiceName, adoptServiceCommand.ServiceVersion, workflowInstanceId, Identifier.Name));
                    }
                }

                var resumptionResult = workflowApp.ResumeBookmark(Bookmarks.ReceiveAdoptCommand, adoptServiceCommand);

                switch (resumptionResult)
                {
                    case BookmarkResumptionResult.Success:
                        break;
                    case BookmarkResumptionResult.NotFound:
                        Log.ErrorFormat("Could not resume bookmark 'ReceiveAdoptCommand' because it could not be found.");
                        break;
                    case BookmarkResumptionResult.NotReady:
                        Log.ErrorFormat("Could not resume bookmark 'ReceiveAdoptCommand' because the runtime has not yet created the bookmark.");
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                resetEvent.WaitOne(TimeSpan.FromSeconds(3));
            }
        }

        #region Overrides of ReactorServiceBase

        /// <summary>
        /// Gets or sets the primary input queue.
        /// </summary>
        /// <value>The primary input queue.</value>
        public override PointToPointChannel PrimaryInputChannel
        {
            get { return _primaryInputChannel ?? (_primaryInputChannel = PointToPointChannel.CreateNameForReactorCoreInputChannel(Identifier)); }
            set { _primaryInputChannel = value; }
        }

        protected override void OnStopping()
        {
            base.OnStopping();

            _serviceMonitorWorkflow.RequestStop();
        }

        #endregion

        protected void ReactorCoreStarted(object sender, EventArgs e)
        {
            Started -= ReactorCoreStarted;

            ObtainManagedServiceControllers();
            CreateAndStartMonitorWorkflow();
        }

        #region Private Methods

        private void CreateAndStartMonitorWorkflow()
        {
            var context = new MonitorContext
            {
                CoreDataProvider = _coreDataProvider,
                Logger = Log,
                ReactorCore = this
            };
            _serviceMonitorWorkflow = new ServicesMonitorWorkflow(context);
            _serviceMonitorWorkflow.LoopWorkflow = true;
            _serviceMonitorWorkflow.Start();
        }

        private void ObtainManagedServiceControllers()
        {
            var windowsServices = ServiceController.GetServices();
            var managedServices = GetManagedServices();

            foreach (var managedService in managedServices)
            {
                var service = managedService;
                var instanceFormattedServiceName = service.Identifier.ToServiceInstanceString();

                var controller = windowsServices.FirstOrDefault(s => s.ServiceName == instanceFormattedServiceName);
                if(controller != null && !_serviceControllers.Any(s => s.ServiceName == instanceFormattedServiceName))
                    _serviceControllers.Add(controller);
            }
        }

        private ServiceController GetServiceControllerByName(string serviceInstanceName)
        {
            var svc = _serviceControllers.SingleOrDefault(s => s.ServiceName == serviceInstanceName);

            // If we don't have record of such service, reach out to the OS again 
            if (svc == null)
                ObtainManagedServiceControllers();

            return _serviceControllers.SingleOrDefault(s => s.ServiceName == serviceInstanceName);
        }

        private void CreateIdentifierForThisInstance()
        {
            // Determine ServiceGrid version
            var assembly = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "Reactor.ServiceGrid").Single();
            var actualAssemblyVersion = assembly.GetName().Version;

            Identifier = new ServiceIdentifier
                             {
                                 Name = string.Format("Reactor.Core.{0}", System.Environment.MachineName),
                                 Version = new Version(actualAssemblyVersion.ToString(3))
                             };
        }        

        private void SendRestartServiceFailureAdvisoryMessage(ServiceIdentifier serviceIdentifier, string reason)
        {
            var msg = new RestartServiceFailed
                          {
                              Subject = string.Format("Restart of service named '{0}' failed", serviceIdentifier.Name),
                              Description = string.Format("Reactor Core '{0}' attempted to restart service '{1}' but failed because {2}.", Identifier.Name, serviceIdentifier, reason)
                          };
            Context.ServiceBus.Send(msg);
        }

        private void RunNewAdoptionWorkflow(ServiceIdentifier serviceIdentifier)
        {
            Activity workflow = new AdoptServiceWorkflow();
            dynamic input = new WorkflowArguments();
            input.ServiceIdentifier = serviceIdentifier;

            WorkflowInvoker.Invoke(workflow, input);

            // Store service controller locally if we didnt already have it
            if (_serviceControllers.Any(s => s.ServiceName == serviceIdentifier.ToServiceInstanceString())) return;

            var serviceController = ServiceController.GetServices().SingleOrDefault(s => s.ServiceName == serviceIdentifier.ToServiceInstanceString());
            if(serviceController == null)
            {
                Log.ErrorFormat("AdoptServiceWorkflow completed but no Windows ServiceController found afterward. Service: {0}", serviceIdentifier.ToServiceInstanceString());
                return;
            }

            _serviceControllers.Add(serviceController);
        }

        private static TrackingParticipant GetDefaultTrackingParticipant()
        {
            var trackingProfile = new TrackingProfile();
            trackingProfile.Queries.Add(new WorkflowInstanceQuery { States = { "*" } });
            trackingProfile.Queries.Add(new ActivityStateQuery
            {
                ActivityName = "*",
                States = { "*" },
                // You can use the following to specify specific stages:
                // States = { ActivityStates.Executing, ActivityStates.Closed }
                Variables = { "*" } // or you can enter specific variable names instead of "*"
            });
            trackingProfile.Queries.Add(new CustomTrackingQuery
            {
                ActivityName = "*",
                Name = "*"
            });

            var trackingParticipant = new LogTrackingParticipant(Log);
            trackingParticipant.TrackingProfile = trackingProfile;

            return trackingParticipant;
        }

        private static void RunNewOrphanWorkflow(ServiceIdentifier serviceIdentifier, Guid serviceInstanceIdentifier)
        {
            Activity workflow = new OrphanServiceWorkflow();
            dynamic input = new WorkflowArguments();
            input.ServiceIdentifier = serviceIdentifier;
            input.ServiceInstanceIdentifier = serviceInstanceIdentifier;

            var workflowApplication = new WorkflowApplication(workflow, input)
            {
                OnUnhandledException = e =>
                    {
                        Log.Error("Unhandled exception thrown while executing OrphanServiceWorkflow.", e.UnhandledException);

                        // Instruct the runtime to terminate the workflow.
                        return UnhandledExceptionAction.Terminate;
                    },
                Completed = e =>
                    {
                        if (e.CompletionState == ActivityInstanceState.Faulted)
                            Log.Error("OrphanServiceWorkflow completed in a faulted state.", e.TerminationException);
                        else
                            Log.Info("OrphanServiceWorkflow completed.");
                    },
                Aborted = e => Log.ErrorFormat("OrphanServiceWorkflow aborted. Reason: {0}", e.Reason)
            };

            workflowApplication.Run(TimeSpan.FromMinutes(5));
        }

        private static WorkflowApplication GetTransferServiceWorkflow(AutoResetEvent resetEvent = null, dynamic inputs = null)
        {
            // Obtain instance store and promote 'DonorCoreName' so we can query persistence for it
            var connectionString = Context.ServiceBus.Configuration.ConnectionData.DatabaseConnectionString;
            var store = new SqlWorkflowInstanceStore(connectionString);

            // Configure instance store
            InstanceHandle handle = store.CreateInstanceHandle();
            InstanceView view = store.Execute(handle, new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
            handle.Free();
            store.DefaultInstanceOwner = view.InstanceOwner;

            // Setup workflow application
            Activity transferWorkflow = new TransferServiceWorkflow();
            var workflowApp = (inputs == null) ? new WorkflowApplication(transferWorkflow) : new WorkflowApplication(transferWorkflow, inputs);
            workflowApp.InstanceStore = store;
            workflowApp.PersistableIdle = e => PersistableIdleAction.Unload;
            workflowApp.OnUnhandledException = e =>
                                                   {
                                                       Log.Error("Unhandled exception thrown while executing TransferServiceWorkflow.", e.UnhandledException);

                                                       // Instruct the runtime to terminate the workflow.
                                                       return UnhandledExceptionAction.Terminate;
                                                   };
            workflowApp.Completed = e =>
                                        {
                                            if (e.CompletionState == ActivityInstanceState.Faulted)
                                                Log.Error("TransferServiceWorkflow completed in a faulted state.", e.TerminationException);
                                            else
                                                Log.Info("TransferServiceWorkflow completed.");
                                        };
            workflowApp.Unloaded = e => { if(resetEvent != null) resetEvent.Set(); };
            workflowApp.Aborted = e => Log.ErrorFormat("TransferServiceWorkflow aborted. Reason: {0}", e.Reason);

            workflowApp.Extensions.Add(GetDefaultTrackingParticipant());
            workflowApp.Extensions.Add(new PersistedPropertiesExtension(Context.ServiceLocator.GetInstance<PlatformConnectionData>()));

            return workflowApp;
        }

        private static bool TryLoadWorkflowInstanceWithRetry(WorkflowApplication workflowApplication, Guid workflowId)
        {
            try
            {
                workflowApplication.Load(workflowId, TimeSpan.FromSeconds(30));
                return true;
            }
            catch (Exception e)
            {
                Log.WarnFormat("Unable to load workflow instance yet. It's possible the workflow instance has not yet been persisted. Make note that an Exception will likely follow this message. Will retry again.");

                Thread.Sleep(TimeSpan.FromSeconds(2));
                return false;
            }
        }

        #endregion
    }
}
